using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using ImpromptuInterface;
using ImpromptuInterface.Dynamic;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;

namespace MongoDB.Dynamic
{
    public class DynamicCollection<TContract> : DynamicCollectionBase
        where TContract : class
    {
        readonly PropertyInfo[] _properties = typeof(TContract)
               .GetProperties(BindingFlags.Instance | BindingFlags.Public);

        private readonly bool _eagerLoad;


        internal DynamicCollection(string keyName = null, bool notifyPropertyChanged = false, bool audit = false)
            : this(typeof(TContract).Name, keyName, notifyPropertyChanged, audit)
        {
        }

        internal DynamicCollection(string collectionName, string keyName = null, bool notifyEnabled = false, bool audit = false)
            : base(collectionName, keyName, notifyEnabled, audit)
        {

            CollectionQueryMethodInfo = Utilities.GetMethodInfo<DynamicCollection<TContract>>(x => x.CollectionQuery(null, 0, null));

            GetByKeyMethodInfo = Utilities.GetMethodInfo<DynamicCollection<TContract>>(x => x.GetByKey(null));

            _eagerLoad = Dynamic.Config.IsEagerLoadEnabled<TContract>();

            foreach (var propertyInfo in _properties.Where(propertyInfo => propertyInfo.CanWrite && !propertyInfo.PropertyType.IsInterface))
            {
                DocumentKeys.Add(propertyInfo.Name);
            }

            /* Eligible Key Order:
             * 0 - by constructor parameter
             * 1 - by config entry
             * 2 - Attribute Count > 0 at key property
             * 3 - By Convention Id
             * 4 - First Property Found by GetProperties
             */

            if (string.IsNullOrEmpty(KeyName))
            {
                if (!Dynamic.Config.TryGetKeyName<TContract>(out KeyName))
                {
                    var key = _properties.Where(p => p.GetCustomAttributes(true).Count() > 0).FirstOrDefault();
                    KeyName = key != null
                                  ? key.Name
                                  : _properties.Where(p => p.Name == "Id").Any()
                                        ? "Id"
                                        : _properties[0].Name;
                }
            }

            if (_properties.Where(p => p.Name == KeyName).FirstOrDefault() == null)
                throw new ArgumentOutOfRangeException("keyName");

            KeyType = typeof(TContract).GetProperty(KeyName).PropertyType;

            DocumentKeys.RemoveAll(p => p == KeyName);

            Initialize();
        }

        /// <summary>
        /// Configures friend collections for querying related documents when necessary (eager load).
        /// This method must be run after construction/ instantiation to avoid infinite loops
        /// </summary>
        internal void PrepareEagerLoad()
        {
            if (!_eagerLoad)
                return;

            foreach (var propertyInfo in _properties.Where(p => p.CanWrite && p.PropertyType.IsInterface))
            {
                var type = propertyInfo.PropertyType;
                if (type.IsGenericType)
                {
                    if (type.GetGenericTypeDefinition() == typeof(IEnumerable<>).GetGenericTypeDefinition())
                    {
                        var argu = type.GetGenericArguments().First();
                        var repo = Dynamic.BuildRepository(argu);
                        ChildCollections.Add(argu.Name, repo);
                    }
                }
                else
                {
                    var repo = Dynamic.BuildRepository(type);
                    FkCollections.Add(propertyInfo.Name, repo);
                }
            }
        }

        void Initialize()
        {
            var indexes = Dynamic.Config.GetIndexes<TContract>();
            foreach (var tuple in indexes)
            {
                Collection.EnsureIndex(tuple.Item1, tuple.Item2);
            }
        }

        #region Query

        public TContract GetByKey(object id)
        {
            var document = GetBsonDocumentById(id);
            var entity = document == null
                       ? null
                       : CastSingle(document);

            return entity;
        }

        void TryGetFKs(TContract entity)
        {
            foreach (var keyValuePair in FkCollections)
            {
                var qInfo = Dynamic.Config.GetDefForFK<TContract>(keyValuePair.Key);

                if (qInfo.SimpleProperty == null)
                    continue;

                var fkValue = Impromptu.InvokeGet(entity, qInfo.SimpleProperty);

                if (fkValue == null)
                    continue;

                var mInfo = keyValuePair.Value.GetByKeyMethodInfo;

                var result = mInfo.Invoke(keyValuePair.Value, new object[] { fkValue });
                if (result == null)
                    continue;

                Impromptu.InvokeSet(entity, qInfo.ComplexPropertyName, result);
            }
        }

        void TryGetChildCollections(TContract entity)
        {
            foreach (var keyValuePair in ChildCollections)
            {
                var queryCollectionInfo = Dynamic.Config.GetDefForChildCollection<TContract>(keyValuePair.Key);

                if (queryCollectionInfo.DetailTable == null)
                    continue;

                //id atual
                var currentId = Impromptu.InvokeGet(entity, KeyName);

                var mInfo = keyValuePair.Value.CollectionQueryMethodInfo;
                // GetMethod("CollectionQuery");

                var result = mInfo.Invoke(keyValuePair.Value, new object[] { queryCollectionInfo.DetailKey, ExpressionType.Equal, currentId });

                Impromptu.InvokeSet(entity, queryCollectionInfo.MasterProperty, result);
            }
        }

        public TContract GetFirstOrDefault(Expression<Func<TContract, bool>> id = null)
        {
            return id == null
                ? CastSingle(Collection.FindOne())
                : CustomQuery(id).FirstOrDefault();
        }

        public IEnumerable<TContract> All()
        {
            var documents = Collection.FindAllAs<DynamicDocument>();
            return CastMany(documents);
        }

        public IEnumerable<TContract> CollectionQuery(string memberName, ExpressionType op, object value)
        {
            var bsonValue = BsonValue.Create(value);

            QueryComplete query = null;
            switch (op)
            {
                case ExpressionType.Equal: query = Query.EQ(memberName, bsonValue);
                    break;
                case ExpressionType.GreaterThan:
                    query = Query.GT(memberName, bsonValue);
                    break;
                case ExpressionType.GreaterThanOrEqual:
                    query = Query.GTE(memberName, bsonValue);
                    break;
                case ExpressionType.LessThan:
                    query = Query.LT(memberName, bsonValue);
                    break;
                case ExpressionType.LessThanOrEqual:
                    query = Query.LTE(memberName, bsonValue);
                    break;
            }
            return CastMany(Collection.FindAs<DynamicDocument>(query));
        }

        public IEnumerable<TContract> CustomQuery(Expression<Func<TContract, bool>> expression = null)
        {
            if (expression == null)
                return All();

            var query = QueryBuilder.BuildQuery(expression, KeyName);

            return query == null
                ? Enumerable.Empty<TContract>()
                : CastMany(Collection.FindAs<DynamicDocument>(query));
        }

        private TContract CastSingle(DynamicDocument document)
        {
            if (document == null)
                return null;

            dynamic item = new ImpromptuDictionary(document.ToDictionary());
            var keyValue = document.GetKey();
            Impromptu.InvokeSetIndex(item, KeyName, keyValue);

            var entity = NotifyEnabled
                       ? item.ActLike<TContract>(typeof(INotifyPropertyChanged))
                       : item.ActLike<TContract>();

            if (FkCollections.Count > 0)
            {
                TryGetFKs(entity);
            }

            if (ChildCollections.Count > 0)
            {
                TryGetChildCollections(entity);
            }

            return entity;
        }

        private IEnumerable<TContract> CastMany(IEnumerable<DynamicDocument> documents)
        {
            return documents.Select(CastSingle).AsEnumerable();
        }

        #endregion

        #region Insert/Update

        public TContract New()
        {
            return NotifyEnabled
                       ? new ImpromptuDictionary().ActLike<TContract>(typeof(INotifyPropertyChanged))
                       : ImpromptuDictionary.Create<TContract>();
        }

        public void UpsertDynamic(IDictionary<string, object> item)
        {
            Upsert(item.ActLike<TContract>());
        }

        public void UpsertImpromptu(dynamic item)
        {
            Upsert(new ImpromptuDictionary(item).ActLike<TContract>());
        }

        /// <summary>
        /// Updates or inserts items to the collection.
        /// The action will be determined by the Key value. If null/0, insert. Otherwise, update.
        /// MongoDB will check either.
        /// </summary>
        /// <param name="item"></param>
        public void Upsert(TContract item)
        {
            try
            {
                dynamic entity = item;
                var keyValue = Impromptu.InvokeGet(entity, KeyName);

                if (IsDefaultValue(keyValue))
                {
                    DynamicDocument document = Insert(entity);
                    Impromptu.InvokeSet(item, KeyName, document.GetKey());
                }
                else Update(entity, keyValue);
            }
            catch (MongoSafeModeException mongoSafeModeException)
            {
                Debug.WriteLine(mongoSafeModeException);
            }
            catch (Exception exception)
            {
                Debug.WriteLine(exception);
                throw;
            }
        }


        private DynamicDocument Insert(object entity)
        {
            DynamicDocument newDocument = KeyType == typeof(int)
                ? BuildDocument(entity, IdGenerator.GetNextIdFor(typeof(TContract)))
                : BuildDocument(entity, null);

            Collection.Insert(newDocument);
            return newDocument;
        }

        /// <summary>
        /// Deletes an item without round-trip to DB.
        /// </summary>
        public bool Delete(TContract item)
        {
            var keyValue = Impromptu.InvokeGet(item, KeyName);
            return DeleteByKey(keyValue);
        }

        /// <summary>
        /// Warning, this will drop the entire collection.
        /// Optionally the Key cache / generator can be erased.
        /// </summary>
        /// <param name="resetCounter"></param>
        public void RemoveAll(bool resetCounter = false)
        {
            Collection.Drop();
            if (resetCounter)
            {
                IdGenerator.ResetCounter<TContract>();
            }
        }

        #endregion

    }
}